/*
 * Galaxium Messenger
 * Copyright (C) 2008 Paul Burton <paulburton89@gmail.com>
 * 
 * License: GNU General Public License (GPL)
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; either version 2 of the License, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

#define DEBUGWHILE

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;

using Anculus.Core;

using Galaxium.Core;
using Galaxium.Client;
using Galaxium.Gui;
using Galaxium.Protocol;

namespace Galaxium.AdiumThemes
{
	public class AdiumMessageDisplay : IMessageDisplay
	{
		IHTMLWidget _htmlWidget;
		IConversation _conversation;
		
		AdiumMessageStyle _style;
		string _variant;
		
		bool _lastContext;
		string _lastUid;
		
		AdiumSubstitutionCollection _headerFooterSubstitutions = new AdiumSubstitutionCollection ();
		AdiumSubstitutionCollection _messageSubstitutions = new AdiumSubstitutionCollection ();
		AdiumSubstitutionCollection _statusSubstitutions = new AdiumSubstitutionCollection ();
		
		public AdiumMessageStyle Style
		{
			get { return _style; }
			set
			{
				ThrowUtility.ThrowIfNull ("style", value);
				
				if (_style == value)
					return;
				
				_style = value;

				if (string.IsNullOrEmpty (_variant) || (!_style.Variants.Contains (_variant)))
					_variant = _style.DefaultVariant;
				
				Reload ();
			}
		}
		
		public string Variant
		{
			get { return _variant; }
			set
			{
				if (_variant == value)
					return;
				
				_variant = value;
				
				if (string.IsNullOrEmpty (_variant) || (!_style.Variants.Contains (_variant)))
					_variant = _style.DefaultVariant;
				
				Reload ();
			}
		}
		
		public object Widget
		{
			get { return _htmlWidget; }
		}
		
		public AdiumMessageDisplay (IConversation conversation)
		{
			ThrowUtility.ThrowIfNull ("conversation", conversation);
			_conversation = conversation;
			
			_htmlWidget = HTMLUtility.CreateHTMLWidget ();
			ThrowUtility.ThrowIfNull ("htmlWidget", _htmlWidget);
			
			_htmlWidget.LinkClicked += delegate (object sender, LinkEventArgs args)
			{
				BaseUtility.SafeOpenURL (args.URL);
				
				// Don't navigate
				return false;
			};
			_htmlWidget.Ready += delegate { Reload (); };
			
			_style = AdiumMessageStyleFactory.ActiveStyle;
			_variant = AdiumMessageStyleFactory.ActiveVariant;
			
			_headerFooterSubstitutions["chatName"] = new SubstitutionDelegate (delegate { return HtmlStripEncode (_conversation.PrimaryContact, (_conversation.ContactCollection.Count > 1) ? "Group Chat" : _conversation.PrimaryContact.DisplayIdentifier); });
			_headerFooterSubstitutions["sourceName"] = new SubstitutionDelegate (delegate { return GenerateNameHtml (_conversation.Session.Account, _conversation.Session.Account.UniqueIdentifier); });
			_headerFooterSubstitutions["destinationName"] = new SubstitutionDelegate (delegate { return HtmlEncode (_conversation.PrimaryContact.UniqueIdentifier); });
			_headerFooterSubstitutions["destinationDisplayName"] = new SubstitutionDelegate (delegate { return HtmlStripEncode (_conversation.PrimaryContact, _conversation.PrimaryContact.DisplayIdentifier); });
			_headerFooterSubstitutions["incomingIconPath"] = new SubstitutionDelegate (delegate { return (_conversation.PrimaryContact.DisplayImage != null) ? _conversation.PrimaryContact.DisplayImage.Filename : _style.DefaultIncomingDisplayIconFilename; });
			_headerFooterSubstitutions["outgoingIconPath"] = new SubstitutionDelegate (delegate { return (_conversation.Session.Account.DisplayImage != null) ? _conversation.Session.Account.DisplayImage.Filename : _style.DefaultOutgoingDisplayIconFilename; });
			_headerFooterSubstitutions["timeOpened"] = new SubstitutionDelegate (delegate (string param, object val) { return HtmlEncode (FormatTime (DateTime.Now, param)); });
			
			/*TODO:
    * %senderColor% - A color derived from the user's name. If a colon separated list of html colors is at Incoming/SenderColors.txt it will be used instead of the default colors. (Adium 1.0+)
    * %senderStatusIcon% - The path to the status icon of the sender (available, away, etc...) (Adium 1.0+)
    * %textbackgroundcolor{X}% - documentation forthcoming */
			/*
    * %messageClasses% - A space separated list of type information for messages, suitable for use as a class attribute. Currently available types are listed below. ("Adium 1.1+")
          o history - the message is from a previous conversation
          o outgoing - the message is being sent from this computer
          o incoming - the message is being sent to this computer
          o message - the message is actually a message, and not something like a status change (note: the exact meaning of this with regard to things like file transfers may change in a future version of Adium)
          o autoreply - the message is an automatic response, generally due to an away status
          o status - the message is a status change
          o event - the message is a notification of something happening (for example, encryption being turned on) */
			
			_messageSubstitutions["message"] = new SubstitutionDelegate (delegate (string param, object val) { return GenerateMessageHtml (val as ITextMessage); });
			_messageSubstitutions["messageDirection"] = new SubstitutionDelegate (delegate (string param, object val) { return "ltr"; });
			_messageSubstitutions["time"] = new SubstitutionDelegate (delegate (string param, object val) { return HtmlEncode (FormatTime ((val as ITextMessage).TimeStamp, param)); });
			_messageSubstitutions["shortTime"] = new SubstitutionDelegate (delegate (string param, object val) { return HtmlEncode ((val as ITextMessage).TimeStamp.ToShortTimeString ()); });
			_messageSubstitutions["service"] = new SubstitutionDelegate (delegate (string param, object val) { return HtmlEncode ((val as ITextMessage).Source.Session.Account.Protocol.Name); });
			_messageSubstitutions["sender"] = new SubstitutionDelegate (delegate (string param, object val) { return HtmlStripEncode ((val as ITextMessage).Source, (val as ITextMessage).Source.DisplayIdentifier); });
			_messageSubstitutions["senderDisplayName"] = new SubstitutionDelegate (delegate (string param, object val) { return HtmlStripEncode ((val as ITextMessage).Source, (val as ITextMessage).Source.DisplayName); });
			_messageSubstitutions["senderScreenName"] = new SubstitutionDelegate (delegate (string param, object val) { return HtmlEncode ((val as ITextMessage).Source.UniqueIdentifier); });
			_messageSubstitutions["userIconPath"] = new SubstitutionDelegate (delegate (string param, object val) { return ((val as ITextMessage).Source.DisplayImage != null) ? (val as ITextMessage).Source.DisplayImage.Filename : ((val as ITextMessage).Source.Local ? _style.DefaultOutgoingDisplayIconFilename : _style.DefaultIncomingDisplayIconFilename); });

			/* TODO:
    * %messageClasses% - A space separated list of type information for messages, suitable for use as a class attribute. Currently available types are listed below. ("Adium 1.1+")
          o history - the message is from a previous conversation
          o outgoing - the message is being sent from this computer
          o incoming - the message is being sent to this computer
          o message - the message is actually a message, and not something like a status change (note: the exact meaning of this with regard to things like file transfers may change in a future version of Adium)
          o autoreply - the message is an automatic response, generally due to an away status
          o status - the message is a status change
          o event - the message is a notification of something happening (for example, encryption being turned on) */
			
			_statusSubstitutions["message"] = new SubstitutionDelegate (delegate (string param, object val) { return HtmlEncode (val as string); });
			_statusSubstitutions["time"] = new SubstitutionDelegate (delegate (string param, object val) { return HtmlEncode (FormatTime (DateTime.Now, param)); });
			_statusSubstitutions["shortTime"] = new SubstitutionDelegate (delegate (string param, object val) { return HtmlEncode (DateTime.Now.ToShortTimeString ()); });
		}
		
		public void AddMessage (ITextMessage message)
		{
			AddMessage (message, false);
		}
		
		public void AddHistoryMessage (ITextMessage message)
		{
			AddMessage (message, true);
		}
		
		void AddMessage (ITextMessage message, bool context)
		{
			if (_style == null)
			{
				Log.Error ("No Style");
				return;
			}
			
			bool newContact = (message.Source.UniqueIdentifier != _lastUid) || (context != _lastContext);
			string html = _style.GenerateMessageHtml (message, newContact, context, _messageSubstitutions);
			string function = newContact ? "appendMessage" : "appendNextMessage";
			
			_lastUid = message.Source.UniqueIdentifier;
			_lastContext = context;
			
			_htmlWidget.RunJavaScript (string.Format ("{0} ('{1}');", function, html));
		}
		
		public void AddOfflineMessage (ITextMessage message)
		{
			//TODO: do something to show a difference from a regular message
			AddMessage (message);
		}
		
		public void AddImageMessage (IEntity from, byte[] data)
		{
			
		}
		
		public void AddSystemMessage (string message)
		{
			if (_style == null)
			{
				Log.Error ("No Style");
				return;
			}
			
			_lastUid = string.Empty;

			string html = _style.GenerateStatusHtml (message, _statusSubstitutions);
			_htmlWidget.RunJavaScript (string.Format ("appendMessage ('{0}');", html));
		}
		
		public void Clear ()
		{
			Reload ();
		}
		
		public void UpdateEmoticon (IEmoticon emot)
		{
			//getElementsByClassName
            //Written by Jonathan Snook, http://www.snook.ca/jonathan
            //Add-ons by Robert Nyman, http://www.robertnyman.com
			
			string js = 
				    "function getElementsByClassName(oElm, strTagName, strClassName)" +
					"{" +
					"    var arrElements = (strTagName == '*' && oElm.all)? oElm.all : oElm.getElementsByTagName(strTagName);" +
					"    var arrReturnElements = new Array();" +
				   @"    strClassName = strClassName.replace(/\-/g, '\\-');" +
					"    var oRegExp = new RegExp('(^|\\s)' + strClassName + '(\\s|$)');" +
					"    var oElement;" +
					"    for(var i=0; i<arrElements.length; i++)" +
					"    {" +
					"        oElement = arrElements[i];" +
					"        if(oRegExp.test(oElement.className))" +
					"        {" +
					"            arrReturnElements.push(oElement);" +
					"        }" +
					"    }" +
					"    return (arrReturnElements)" +
					"}" +
					
					"var emoticons = getElementsByClassName(document, 'img', 'galaxiumEmoticon');" +
					"for (var i = 0; i < emoticons.length; i++)" +
					"{" +
					"    var url = unescape(emoticons[i].src).replace('file://', '');" +
					"    if (url != '" + emot.Filename + "')" +
					"        continue;" +
					"    emoticons[i].src = url;" +
					"    emoticons[i].style.width = '" + emot.Width.ToString () + "px';" +
					"    emoticons[i].style.height = '" + emot.Height.ToString () + "px';" +
					"}" +
					"scrollToBottom();";
			
			_htmlWidget.RunJavaScript (js);
		}
		
		void Reload ()
		{
			_style = AdiumMessageStyleFactory.ActiveStyle;
			_variant = AdiumMessageStyleFactory.ActiveVariant;
			
			if (_style != null)
			{
				string filename = _style.GenerateBaseHtmlFile (_variant, _headerFooterSubstitutions);
				//Log.Debug ("Filename: {0}", filename);
				_htmlWidget.LoadUrl (filename);
			}
			else
			{
				string html = string.Format ("<h1>Unable to load a style</h1><p>Galaxium was unable to load a message style from the directory {0}</p>",
				                             AdiumMessageStyleFactory._systemThemePath);
				
				_htmlWidget.LoadHtml (html);
			}
		}
		
		string GenerateMessageHtml (ITextMessage msg)
		{
			if (msg == null || msg.Chunks == null)
				return string.Empty;
			
			return GenerateHtml (msg.Chunks);
		}
		
		string GenerateHtml (IEnumerable<ITextChunk> chunks)
		{
			string result = string.Empty;
			
			foreach (ITextChunk chunk in chunks)
			{
				switch (chunk.Type)
				{
					case TextChunkType.Text:
						result += ApplyStyle (HtmlEncodeMessage (chunk.Text), chunk.Style);
						break;
					case TextChunkType.URL:
						string url = chunk.Text;
						result += string.Format ("<a href=\"{0}\">{1}</a>", XmlUtility.GetEncodedString (url.Trim ()), ApplyStyle (HtmlEncodeMessage (url), chunk.Style));
						break;
					case TextChunkType.Emoticon:
					
						IEmoticon emot = ((EmoticonTextChunk)chunk).Emoticon;
					
						string html = "<img src=\"";
					
						if (!string.IsNullOrEmpty (emot.Filename))
							html += emot.Filename;
						else
							Log.Warn("Emoticon With No Filename! " + emot.Name);
					
						html += "\" class=\"galaxiumEmoticon\" style=\"";
				 
						if (emot.Width > 0)
							html += string.Format ("width: {0}px; ", emot.Width);
						if (emot.Height > 0)
							html += string.Format ("height: {0}px; ", emot.Height);

						html += string.Format ("\" title=\"{0} ({1})\"/>", HtmlEncode (emot.Name), HtmlEncode (chunk.Text));
						
						result += html;
					
						break;
				}
			}
			
			return result;
		}
		
		string HtmlStripEncode (IEntity entity, string txt)
		{
			if (entity == null)
				return HtmlEncode (txt);
			
			return HtmlEncode (MessageUtility.StripMarkup (txt, entity.Session.Account.Protocol));
		}
		
		string HtmlEncode (string str)
		{
			string encoded = System.Web.HttpUtility.HtmlEncode (str);
			
			encoded = BaseUtility.ReplaceString (encoded, "'", "&apos;", true);
			encoded = BaseUtility.ReplaceString (encoded, "\r\n", "<br/>", true);
			encoded = BaseUtility.ReplaceString (encoded, "\n", "<br/>", true);
			encoded = BaseUtility.ReplaceString (encoded, "\r", "<br/>", true);
			encoded = BaseUtility.ReplaceString (encoded, "\\", "\\\\", true);
			
			return encoded;
		}
		
		string HtmlEncodeMessage (string str)
		{
			string ret = HtmlEncode (str);
			
			// Encode messages as adium does in AIHTMLDecoder.m
			
			ret = ret.Replace ("\t", " &nbsp;&nbsp;&nbsp;");
			
			if (ret.StartsWith (" "))
				ret = "&nbsp;" + ret.Substring (1);
			
			ret = ret.Replace ("  ", " &nbsp;");
			
			return ret;
		}
		
		string GenerateNameHtml (IEntity entity, string str)
		{
			List<ITextChunk> chunks = MessageUtility.SplitMessage (str, new TextStyle (), entity.Session.Account.Protocol, entity, null);
			return GenerateHtml (chunks);
		}
		
		string FormatTime (DateTime time, string format)
		{
			if (string.IsNullOrEmpty (format))
				return time.ToLongTimeString ();
			
			int i = 0;
			string ret = string.Empty;
			
#if DEBUGWHILE
			int iterations = 0;
#endif
			
			while (i < format.Length)
			{
#if DEBUGWHILE
				iterations++;
				
				if (iterations > 100)
					Log.Warn ("Iteration {0}, pos {1} format '{2}'", iterations, i, format);
#endif
				
				if (format[i] != '%')
				{
					ret += format[i++];
					continue;
				}
				
				char c = (format.Length > i + 1) ? format[i + 1] : '\0';
				
				switch (c)
				{
				case '%':
					ret += '%';
					break;
				case 'a':
					ret += time.ToString ("ddd");
					break;
				case 'A':
					ret += time.ToString ("dddd");
					break;
				case 'b':
					ret += time.ToString ("MMM");
					break;
				case 'B':
					ret += time.ToString ("MMMM");
					break;
				case 'c':
					ret += time.ToString ("hh:mm:ss tt dd/MM/yyyy zzz");
					break;
				case 'd':
					ret += time.ToString ("dd");
					break;
				case 'e':
					ret += time.ToString ("d");
					break;
				case 'F':
					ret += time.ToString ("FFF");
					break;
				case 'H':
					ret += time.ToString ("HH");
					break;
				case 'I':
					ret += time.ToString ("hh");
					break;
				case 'j':
					ret += time.DayOfYear.ToString ();
					break;
				case 'm':
					ret += time.ToString ("MM");
					break;
				case 'M':
					ret += time.ToString ("mm");
					break;
				case 'p':
					ret += time.ToString ("tt");
					break;
				case 'S':
					ret += time.ToString ("ss");
					break;
				case 'w':
					ret += ((int)time.DayOfWeek).ToString ();
					break;
				case 'x':
					ret += time.ToString ("dd/MM/yyyy zzz");
					break;
				case 'X':
					ret += time.ToString ("hh:mm:ss tt");
					break;
				case 'y':
					ret += time.ToString ("yy");
					break;
				case 'Y':
					ret += time.ToString ("yyyy");
					break;
				case 'Z':
					ret += TimeZone.CurrentTimeZone.StandardName;
					break;
				case 'z':
					ret += time.ToString ("zzz");
					break;
				default:
					break;
				}
				
				i += ((c == '\0') ? 1 : 2);
			}
			
			return ret;
		}
		
		string ApplyStyle (string msg, ITextStyle style)
		{
			StringBuilder sb = new StringBuilder ();
			
			if (style.Bold)
				sb.Append ("<b>");
			if (style.Italic)
				sb.Append ("<i>");
			if (style.Underline)
				sb.Append ("<u>");
			if (style.Strikethrough)
				sb.Append ("<s>");
			
			// The font tag is old and depreciated, but adium uses it, so...
			bool font = false;
			bool fontStyle = false;
			
			if (!style.IsDefaultFont)
			{
				if (!font)
				{
					sb.Append ("<font ");
					font = true;
				}
				
				sb.AppendFormat ("face=\"{0}\" ", style.Font);
			}
			
			if (style.ForeColor >= 0)
			{
				if (!font)
				{
					sb.Append ("<font ");
					font = true;
				}
				
				sb.AppendFormat ("color=\"#{0:x6}\" ", style.ForeColor);
			}
			
			if (style.FontSize > 0)
			{
				if (!font)
				{
					sb.Append ("<font ");
					font = true;
				}
				
				sb.AppendFormat ("absz=\"{0}\" size=\"{1}\" ", style.FontSize, PointsToHTML (style.FontSize));
			}
			
			if (style.BackColor >= 0)
			{
				if (!fontStyle)
				{
					if (!font)
					{
						sb.Append ("<font ");
						font = true;
					}
					
					sb.Append ("style=\"");
					fontStyle = true;
				}
				
				sb.AppendFormat ("background-color: #{0:x6}; ", style.BackColor);
			}
			
			if (fontStyle)
				sb.Append ("\"");
			if (font)
				sb.Append (">");
			
			sb.Append (msg);
			
			if (font)
				sb.Append ("</font>");
			
			if (style.Strikethrough)
				sb.Append ("</s>");
			if (style.Underline)
				sb.Append ("</u>");
			if (style.Italic)
				sb.Append ("</i>");
			if (style.Bold)
				sb.Append ("</b>");
			
			return sb.ToString ();
		}
		
		int PointsToHTML (int points)
		{
			if (points <= 9)
				return 1;
			if (points <= 10)
				return 2;
			if (points <= 12)
				return 3;
			if (points <= 14)
				return 4;
			if (points <= 18)
				return 5;
			if (points <= 24)
				return 6;
			
			return 7;
		}
	}
}
